import requests
from bs4 import BeautifulSoup
import logging
import pandas as pd
import hashlib
import time
import re

class MusicDownloader:
    def __init__(self, search_word, appid='16073360', secret='0b50b02fd0d73a9c4c8c3a781c30845f'):
        self.search_word = search_word
        self.base_url = 'https://music.91q.com'
        self.appid = appid
        self.secret = secret
        self.headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
        }

    def _generate_timestamp(self):
        return int(time.time())

    def _create_signature(self, params):
        param_str = '&'.join([f'{k}={v}' for k,v in sorted(params.items())])
        sign_str = f'{param_str}{self.secret}'
        return hashlib.md5(sign_str.encode('utf-8')).hexdigest()

    def _get_total_pages(self):
        try:
            response = requests.get(f'{self.base_url}/search', params={'word': self.search_word}, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            pagination = soup.find('ul', {'class': 'el-pager'})
            return int(pagination.find_all('li')[-1].text) if pagination else 1
        except Exception as e:
            logging.error(f"获取总页数失败: {e}")
            return 1

    def _fetch_page_data(self, page):
        params = {
            'word': self.search_word,
            'type': 1,
            'pageNo': page,
            'pageSize': 20,
            'appid': self.appid,
            'timestamp': self._generate_timestamp()
        }
        params['sign'] = self._create_signature(params)

        try:
            response = requests.get(f'{self.base_url}/v1/search', params=params, headers=self.headers)
            response.raise_for_status()
            return self._parse_song_data(response.text)
        except Exception as e:
            logging.error(f"第{page}页数据获取失败: {e}")
            return pd.DataFrame()

    def _parse_song_data(self, html):
        soup = BeautifulSoup(html, 'html.parser')
        songs = []
        
        for item in soup.select('li.pr.t.clearfix'):
            song_link = item.select_one('.song-box a')
            tsid = song_link['href'].split('/')[-1]
            
            songs.append({
                'name': song_link.text.strip(),
                'tsid': tsid,
                'singer': '、'.join([a.text for a in item.select('.artist a')]),
                'album': item.select_one('.album a').text
            })
        
        return pd.DataFrame(songs)

    def _get_download_url(self, tsid):
        params = {
            'appid': self.appid,
            'TSID': tsid,
            'timestamp': self._generate_timestamp()
        }
        params['sign'] = self._create_signature(params)

        try:
            response = requests.get(f'{self.base_url}/v1/song/tracklink', params=params, timeout=10)
            data = response.json()
            return data.get('data', {}).get('path')
        except Exception as e:
            logging.error(f"TSID:{tsid} 获取下载链接失败: {e}")
            return None

    def download_all(self):
        total_pages = self._get_total_pages()
        all_songs = pd.concat([self._fetch_page_data(page) for page in range(1, total_pages+1)])

        for _, song in all_songs.iterrows():
            mp3_url = self._get_download_url(song['tsid'])
            if mp3_url:
                self._save_mp3(song['name'], song['tsid'], mp3_url)

    def _save_mp3(self, song_name, tsid, url):
        safe_name = re.sub(r'[\\/*?:"<>|]', '_', song_name)
        filename = f"{safe_name}_{tsid}.mp3"

        try:
            response = requests.get(url, headers=self.headers, stream=True)
            with open(filename, 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        f.write(chunk)
            logging.info(f"成功下载: {filename}")
        except Exception as e:
            logging.error(f"下载{filename}失败: {e}")

if __name__ == "__main__":
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers=[logging.StreamHandler()]
    )

    search_term = input("请输入歌手或歌曲名称：")
    downloader = MusicDownloader(search_term)
    downloader.download_all()